<?php

namespace Symfony\Components\Console\Output;

/*
 * This file is part of the symfony framework.
 *
 * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
 *
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE.
 */

/**
 * Base class for output classes.
 *
 * There is three level of verbosity:
 *
 *  * normal: no option passed (normal output - information)
 *  * verbose: -v (more output - debug)
 *  * quiet: -q (no output)
 *
 * @package    symfony
 * @subpackage console
 * @author     Fabien Potencier <fabien.potencier@symfony-project.com>
 */
abstract class Output implements OutputInterface
{
  const VERBOSITY_QUIET   = 0;
  const VERBOSITY_NORMAL  = 1;
  const VERBOSITY_VERBOSE = 2;

  const OUTPUT_NORMAL = 0;
  const OUTPUT_RAW = 1;
  const OUTPUT_PLAIN = 2;

  protected $verbosity;
  protected $decorated;

  static protected $styles = array(
    'error'    => array('bg' => 'red', 'fg' => 'white'),
    'info'     => array('fg' => 'green'),
    'comment'  => array('fg' => 'yellow'),
    'question' => array('bg' => 'cyan', 'fg' => 'black'),
  );
  static protected $options    = array('bold' => 1, 'underscore' => 4, 'blink' => 5, 'reverse' => 7, 'conceal' => 8);
  static protected $foreground = array('black' => 30, 'red' => 31, 'green' => 32, 'yellow' => 33, 'blue' => 34, 'magenta' => 35, 'cyan' => 36, 'white' => 37);
  static protected $background = array('black' => 40, 'red' => 41, 'green' => 42, 'yellow' => 43, 'blue' => 44, 'magenta' => 45, 'cyan' => 46, 'white' => 47);

  /**
   * Constructor.
   *
   * @param integer $verbosity The verbosity level (self::VERBOSITY_QUIET, self::VERBOSITY_NORMAL, self::VERBOSITY_VERBOSE)
   * @param Boolean $decorated Whether to decorate messages or not (null for auto-guessing)
   */
  public function __construct($verbosity = self::VERBOSITY_NORMAL, $decorated = null)
  {
    $this->decorated = (Boolean) $decorated;
    $this->verbosity = null === $verbosity ? self::VERBOSITY_NORMAL : $verbosity;
  }

  /**
   * Sets a new style.
   *
   * @param string $name    The style name
   * @param array  $options An array of options
   */
  static public function setStyle($name, $options = array())
  {
    static::$styles[strtolower($name)] = $options;
  }

  /**
   * Sets the decorated flag.
   *
   * @param Boolean $decorated Whether to decorated the messages or not
   */
  public function setDecorated($decorated)
  {
    $this->decorated = (Boolean) $decorated;
  }

  /**
   * Gets the decorated flag.
   *
   * @return Boolean true if the output will decorate messages, false otherwise
   */
  public function isDecorated()
  {
    return $this->decorated;
  }

  /**
   * Sets the verbosity of the output.
   *
   * @param integer $level The level of verbosity
   */
  public function setVerbosity($level)
  {
    $this->verbosity = (int) $level;
  }

  /**
   * Gets the current verbosity of the output.
   *
   * @return integer The current level of verbosity
   */
  public function getVerbosity()
  {
    return $this->verbosity;
  }

  /**
   * Writes a message to the output and adds a newline at the end.
   *
   * @param string|array $messages The message as an array of lines of a single string
   * @param integer      $type     The type of output
   */
  public function writeln($messages, $type = 0)
  {
    $this->write($messages, true, $type);
  }

  /**
   * Writes a message to the output.
   *
   * @param string|array $messages The message as an array of lines of a single string
   * @param Boolean      $newline  Whether to add a newline or not
   * @param integer      $type     The type of output
   */
  public function write($messages, $newline = false, $type = 0)
  {
    if (self::VERBOSITY_QUIET === $this->verbosity)
    {
      return;
    }

    if (!is_array($messages))
    {
      $messages = array($messages);
    }

    foreach ($messages as $message)
    {
      switch ($type)
      {
        case Output::OUTPUT_NORMAL:
          $message = $this->format($message);
          break;
        case Output::OUTPUT_RAW:
          break;
        case Output::OUTPUT_PLAIN:
          $message = strip_tags($this->format($message));
          break;
        default:
          throw new \InvalidArgumentException(sprintf('Unknown output type given (%s)', $type));
      }

      $this->doWrite($message, $newline);
    }
  }

  /**
   * Writes a message to the output.
   *
   * @param string  $message A message to write to the output
   * @param Boolean $newline Whether to add a newline or not
   */
  abstract public function doWrite($message, $newline);

  /**
   * Formats a message according to the given styles.
   *
   * @param  string $message The message to style
   *
   * @return string The styled message
   */
  protected function format($message)
  {
    $message = preg_replace_callback('#<([a-z][a-z0-9\-_]+)>#i', array($this, 'replaceStartStyle'), $message);

    return preg_replace_callback('#</([a-z][a-z0-9\-_]+)>#i', array($this, 'replaceEndStyle'), $message);
  }

  protected function replaceStartStyle($match)
  {
    if (!$this->decorated)
    {
      return '';
    }

    if (!isset(static::$styles[strtolower($match[1])]))
    {
      throw new \InvalidArgumentException(sprintf('Unknown style "%s".', $match[1]));
    }

    $parameters = static::$styles[strtolower($match[1])];
    $codes = array();

    if (isset($parameters['fg']))
    {
      $codes[] = static::$foreground[$parameters['fg']];
    }

    if (isset($parameters['bg']))
    {
      $codes[] = static::$background[$parameters['bg']];
    }

    foreach (static::$options as $option => $value)
    {
      if (isset($parameters[$option]) && $parameters[$option])
      {
        $codes[] = $value;
      }
    }

    return "\033[".implode(';', $codes)."m";
  }

  protected function replaceEndStyle($match)
  {
    if (!$this->decorated)
    {
      return '';
    }

    return "\033[0m";
  }
}
